Skip to content

gh-145678: Fix use-after-free in itertools.groupby _grouper_next()#145679

Open
sampsonc wants to merge 9 commits intopython:mainfrom
sampsonc:gh-145678-grouper-uaf
Open

gh-145678: Fix use-after-free in itertools.groupby _grouper_next()#145679
sampsonc wants to merge 9 commits intopython:mainfrom
sampsonc:gh-145678-grouper-uaf

Conversation

@sampsonc
Copy link

@sampsonc sampsonc commented Mar 9, 2026

Summary

Fixes a use-after-free (UAF) in _grouper_next() in Modules/itertoolsmodule.c.

Root Cause

_grouper_next() passed igo->tgtkey and gbo->currkey directly to
PyObject_RichCompareBool() without first holding strong references.
A user-defined __eq__ can re-enter the parent groupby iterator during
the comparison. That re-entry calls groupby_step(), which executes:

Py_XSETREF(gbo->currkey, newkey);

This frees gbo->currkey while it is still under comparison — a use-after-free.

Fix

Take INCREF'd local snapshots before calling PyObject_RichCompareBool(),
mirroring the protection added to groupby_next() in gh-143543:

PyObject *tgtkey = igo->tgtkey;
PyObject *currkey = gbo->currkey;
Py_INCREF(tgtkey);
Py_INCREF(currkey);
rcmp = PyObject_RichCompareBool(tgtkey, currkey, Py_EQ);
Py_DECREF(tgtkey);
Py_DECREF(currkey);

Test plan

  • ./python -m test test_itertools -k test_grouper_next_reentrant_eq_does_not_crash
  • Run with AddressSanitizer (./configure --with-pydebug && make with ASAN enabled) to confirm no UAF

Closes gh-145678.

🤖 Generated with Claude Code

@sampsonc sampsonc requested a review from rhettinger as a code owner March 9, 2026 14:42
@python-cla-bot
Copy link

python-cla-bot bot commented Mar 9, 2026

All commit authors signed the Contributor License Agreement.

CLA signed

_grouper_next() passed igo->tgtkey and gbo->currkey directly to
PyObject_RichCompareBool() without first holding strong references.
A re-entrant __eq__ that advanced the parent groupby iterator would
call groupby_step(), which executes Py_XSETREF(gbo->currkey, newkey),
freeing currkey while it was still under comparison.

Fix by taking INCREF'd local snapshots before the comparison, mirroring
the protection added to groupby_next() in pythongh-143543.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sampsonc sampsonc force-pushed the gh-145678-grouper-uaf branch from eb0bb61 to 79bcea0 Compare March 9, 2026 15:12
@sampsonc
Copy link
Author

sampsonc commented Mar 9, 2026

The two CI failures (Windows free-threading arm64 and Docs) are pre-existing issues in main unrelated to this PR. The docs check-warnings.py script confirmed zero new warnings from our changes, and the Windows failure is ENV_CHANGED from test_multiprocessing_spawn.test_threads.

@sampsonc
Copy link
Author

I've been away for a couple of days. Is there anything else that I can do on this one? Thanks!

@picnixz
Copy link
Member

picnixz commented Mar 11, 2026

I've been away for a couple of days. Is there anything else that I can do on this one? Thanks!

Please address the feedback first. I would also suggest that you are aware of https://devguide.python.org/getting-started/generative-ai/ in order to avoid back-and-forth.

sampsonc and others added 6 commits March 12, 2026 07:41
Use a single variable `g` instead of `outer_grouper`/`g`, matching
the style of the sibling test test_groupby_reentrant_eq_does_not_crash.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The iterator is never exhausted at this point, so StopIteration
cannot be raised.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
…rouper-uaf.rst

Co-authored-by: Petr Viktorin <encukou@gmail.com>
@sampsonc
Copy link
Author

All feedback has been addressed.

@encukou
Copy link
Member

encukou commented Mar 13, 2026

I couldn't get the test to crash with current main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

itertools.groupby: use-after-free in _grouper_next() when __eq__ re-enters the iterator

3 participants